# --------------------------------------------------------------------------------------------------------------------
# Project:             Digital Payment System (DPS)
# Website:             http://code.google.com/p/dps-x509/
# Purpose of document: All the generic functions that apply to x509
# --------------------------------------------------------------------------------------------------------------------
# --------------------------------------------------------------------------------------------------------------------
import sys, re, base64

sys.path.append("..")

from pyasn1.type import base
from pyasn1.codec.der import decoder, encoder

try:
  import keyczar.util as util
except ImportError:
  from keyczar import *

import dps.errors
import dps.utils
import dps.x509Helper

# --------------------------------------------------------------------------------------------------------------------
def sanitizeStr(s):
  """
  adjust string for conversion to bytes. it must have length % 4 = 0 
  @param s: base64 input string
  @type  s: string
  @return: string padded with '=' to be of length divisible by 4 
  @rtype:  string
  """
  s = s.rstrip("=")
  s = str(s.replace(" ", ""))
  s = str(s.replace("\n", ""))
  d = len(s) % 4
  s += "=" * d
  return s  

# --------------------------------------------------------------------------------------------------------------------
def isBase64(s):    
  """ 
  returns true if s does not cotain any non base64 characters
  @rtype: bool
  """
  hexRegex = "^[a-zA-Z0-9\\-\\s\\+/=]*$"
  return not re.match(hexRegex, s) == None

# -------------------------------------------------------------------------------------------------------------------- 
def certsToBase64(cert):    
  """ 
  converts an input cert chain (may be only one) of certificates stripped base64. 
  (ie. contains no header and footer)
  @param   cert: der or base64 encoded certificate
  @type    cert: string or bytes
  @raise   dps.errors.DecodingError: if unable to perform conversion    
  @return: list of base64 encoded certificates in order from top of input
  @rtype:  list
  """
  outlist = []
  try:
    isBase64Cert = isBase64(cert)
    dps.utils.out("X509Asn1Extractor.isBase64: "+str(isBase64Cert))
    certBase64 = ""
    if not isBase64Cert:
      certBase64 = dps.x509Helper.bytesToBase64(cert)
    else:
      certBase64 = cert.replace(" ","")
      certBase64 = certBase64.replace("\n","")
      certBase64 = certBase64.replace("\r","")
      
    dashes = "\\-"*5
    certMatcher = dashes+"BEGINCERTIFICATE(?:REQUEST)?"+dashes+"(.*?)"+dashes+"ENDCERTIFICATE(?:REQUEST)?"+dashes
    templist = re.split(certMatcher, certBase64)
    
    for l in templist:
      if len(l) > 0:
        outlist.append(dps.x509Helper.sanitizeStr(l))
    if len(outlist) == 0:
      #it must be a single item with no header and footer
      outlist.append(dps.x509Helper.sanitizeStr(certBase64))
      
  except Exception, e:      
    dps.utils.out("Exception in X509Helper.certToBase64: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.DecodingError()
  dps.utils.out("X509Helper.certsToBase64: Number of certificates: " + str(len(outlist)))
  return outlist
  
# -------------------------------------------------------------------------------------------------------------------- 
def prettyBase64(base64Text, breakChar="\n", isCsr=False):
  """
  format base64 with begin and end decoration
  @param   base64Text: input text
  @type    base64Text: string
  @param   breakChar: delimiter for string. for example, use '<br/>' for web pages
  @type    breakChar: string
  @param   isCsr: is true uses '-----BEGIN CERTIFICATE REQUEST-----' otherwise uses '-----BEGIN CERTIFICATE-----' 
  @type    isCsr: bool
  @return: formatted text with certifcate headers and footers line breaks every 64 characters
  @rtype:  string
  """
  dps.utils.assertion(isBase64(base64Text), "x509Helper.prettyBase64: Not base64")
  
  isCsrStr = ""
  if isCsr:
    isCsrStr = " REQUEST"
  formattedBase64Text = "-----BEGIN CERTIFICATE%s-----%s" % (isCsrStr, breakChar)
  for i in range(0, len(base64Text), 64):
    formattedBase64Text += base64Text[i:i+64] + breakChar
  formattedBase64Text += "-----END CERTIFICATE%s-----%s" % (isCsrStr, breakChar)
  return formattedBase64Text

# -------------------------------------------------------------------------------------------------------------------- 
def prettyBase64Certificates(base64Certs, breakChar="\n"):
  """
  converts list to formatted certificate list
  @param   base64Certs: input certificates
  @type    base64Certs: list
  @param   breakChar: delimiter for string. for example, use '<br/>' for web pages
  @type    breakChar: string
  @return: a string with line breaks at 64. takes as input multiple certificates in a list.
  this function also wraps the base64 certs in header guards. order is assumed to be correct from top down.
  @rtype:  string  
  """
  dps.utils.assertion(isinstance(base64Certs, list), "x509Helper.prettyBase64Certificates: Not a list")
  ret = ""
  for item in base64Certs:
    ret += prettyBase64(sanitizeStr(item), breakChar)
  return ret

# -------------------------------------------------------------------------------------------------------------------- 
def asn1ToBytes(asn1):
  """ 
  convert asn1 to byte string
  @raise  dps.errors.DecodingError: on failure 
  @rtype: byte string 
  """
  dps.utils.assertion(isinstance(asn1, base.Asn1Item), "X509Helper.asn1ToBytes: Incorrect asn1 type")

  ret = None
  try:
    ret = encoder.encode(asn1)
  except Exception, e:      
    dps.utils.out("Exception in x509Helper.asn1ToBytes: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.DecodingError()
  
  return ret
  
# -------------------------------------------------------------------------------------------------------------------- 
def bytesToAsn1(bytes, spec= None):
  """
  convert byte string to asn1. If spec supplied decoding will contain named types from the spec
  @type   spec: dps.asn1CertificateSpecification.* or None
  @raise  dps.errors.DecodingError: on failure 
  @rtype: byte string
  """
  ret = None
  try:
    if spec == None:
      ret = decoder.decode(bytes)[0]
    else:
      ret = decoder.decode(bytes, asn1Spec=spec)[0]      
  except Exception, e:      
    dps.utils.out("Exception in x509Helper.bytesToAsn1: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.DecodingError()
  
  return ret

# -------------------------------------------------------------------------------------------------------------------- 
def bytesToBase64(bytes, isSafeMode= False):
  """ 
  convert string byte to base64. url safe chars are used in safe mode
  @param   bytes: bytes to be converted
  @type    bytes: byte string
  @param   isSafeMode: if True convert to url safe mode otherwise perform traditional base64 conversion
  @type    isSafeMode: bool
  @raise   dps.errors.DecodingError: on failure 
  @return: base64 string
  @rtype:  string  
  """
  ret = ""
  try:
    if isSafeMode:
      ret = util.Encode(bytes)
    else:
      ret = base64.b64encode(bytes)
      ret = sanitizeStr(ret)
      dps.utils.assertion(isBase64(ret), "x509Helper.bytesToBase64: Error converting to base64")
  except Exception, e:      
    dps.utils.out("Exception in x509Helper.bytesToBase64: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.DecodingError()
  return ret
  
# -------------------------------------------------------------------------------------------------------------------- 
def base64ToBytes(base64Cert, isSafeMode= False):
  """ 
  converts base64 to byte string. url safe chars are decoded in safe mode.
  @param   base64Cert: input text
  @type    base64Cert: string
  @param   isSafeMode: if True convert from url safe mode otherwise perform traditional base64 conversion
  @type    isSafeMode: bool
  @raise dps.errors.DecodingError: on failure 
  @rtype: byte string
  """
  ret = ""
  try:
    base64Cert = sanitizeStr(base64Cert)
    if isSafeMode:
      ret = util.Decode(base64Cert)
    else:
      dps.utils.assertion(isBase64(base64Cert), "x509Helper.base64ToBytes: Not base64")
      ret = base64.b64decode(base64Cert)
  except Exception, e:      
    dps.utils.out("Exception in x509Helper.base64ToBytes: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.DecodingError()
  
  return ret
